<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<title>2048</title>
		<style type="text/css">
			*{
				margin: 0;
				padding: 0;
			}
			#myCanvas{
				border: 1px solid #f40;
			}
			button{
				padding: 10px;
			}
			.ui{
				position: absolute;
				left: 450px;
				top: 0;
				width: 240px;
				height: 400px;
				background-color: #f1f1f1;
				font-size: 14px;
				text-indent: 1em;
				box-sizing: border-box;
				padding-top: 10px;
			}
			.title{
				font-size: 16px;
				font-weight: bold;
			}
			input{
				display: block;
				width: 90%;
				margin: 10px auto;
				border: none;
				outline: none;
				background-color: #fff;
				text-align: center;
				padding: 4px 0;
			}
			.ui button{
				padding: 2px 5px;
				margin-left: 140px;
			}
			.log{
				width: 90%;
				height: 100px;
				background-color: #fff;
				margin: 5px auto;
				box-sizing: border-box;
				padding: 10px;
				font-size: 14px;
				color: #848484;
				font-weight: bold;
				text-indent: 0;
			}
			.btn{
				width: 90%;
				box-sizing: border-box;
				margin: 0 auto;
				text-align: center;
				text-indent: 0;
				font-size: 14px;
				padding: 2px 10px;
				border-radius: 14px;
				border: 1px solid #999;
				background-color: #fff;
				user-select: none;
				cursor: pointer;
			}
			.btn:hover{
				background-color: #eee;
			}
			.btn:active{
				color: #fff;
				background-color: #aaa;
			}
			.btns{
				display: flex;
				width: 600px;
			}
			.btns .btn{
				width: fit-content;
			}
		</style>
	</head>
	<body>
		<canvas id="myCanvas" width="400" height="400"></canvas>
		<div></div>
		<div class="btns">
			<div class="btn" onclick="start()" type="button">开始/重开</div>
			<div class="btn" onclick="test()" type="button">展示所有种类方块</div>
			<div class="btn" onclick="roll()" type="button">滚动摇号(顺时针自动快速点击,需先开始游戏)</div>
			<div class="btn" onclick="stopRoll()" type="button">暂停摇号</div>
		</div>
		
		<div class="ui">
			<div class="title">欢迎试玩 2048</div>
			<div class="desc">横纵方块数量(可设定)：</div>
			<input type="text" id="rowNum" value="4" />
			<div onclick="commit()" class="btn">应用</div>
			<div class="desc" style="color: #888;font-size:12px">tip: 越宽敞，越容易进化出稀有生物</div>
			<div class="desc" style="color: #f40;font-weight: bold;margin-top: 20px;">WASD 或 上下左右 操作</div>
			<div class="desc" style="margin-top: 80px;">信息窗</div>
			<div id="logBoard" class="log"></div>
		</div>
		<script type="text/javascript">
			function test(){
				gameDemo.testAll()
			}
			function roll(){
				gameDemo.roll()
			}
			function stopRoll(){
				gameDemo.stopRoll()
			}
			function start () {
				gameDemo.destroy()
				var n = Number(rowNum.value)
				gameDemo = new Game({n})
				logBoard.innerText = "开始游戏"
			}
			function commit () {
				var temp = Number(rowNum.value)
				if(!Number.isNaN(temp)){
					start()
				}else{
					logBoard.innerText = "请输入正整数(不要太大)"
				}
			}
			rowNum.oninput = e => {
				console.log(e)
			}
			!function(w,e){
				function Game(option){
					Object.assign(this,{
						el: "#myCanvas",
						n: 4,
						mainWidth: 400,
						side: 100,
						cWidth: 90,
						innerOffset: 0,
						timer: null,
						map: [],
						map0: [],
						map1: [],
						map2: [],
						map3: [],
						num: 0,
						bgColor: "#ffece6",
						numColorMap: {
							"2": "#fff8eb",
							"4": "#ffe6cd",
							"8": "#ffd9ac",
							"16": "#ffc08c",
							"32": "#ff9c63",
							"64": "#ff844b",
							"128": "#ff6d3c",
							"256": "#ff4c29",
							"512": "#ff6ebc",
							"1024": "#e173ff",
							"2048": "#8811ff",
						}
					},option)
					if(!this.n) this.n = 4
					this.init()
				}
				Game.prototype.expend = function(obj){Object.assign(this,obj)}
				Game.prototype.expend({
					
					// ********************  基础底层部分
					
					init(){
						this.canvas = e.querySelector(this.el)
						this.canvas.isUsed = true
						this.cx = this.canvas.getContext("2d")
						this.side = this.mainWidth / this.n
						this.cWidth = this.side * .9
						this.innerOffset = (this.side-this.cWidth) / 2
						var fontSize = Math.round(28 * this.side / 100)
						this.cx.font = fontSize + "px 微软雅黑"
						this.cx.textAlign = "center"
						this.cx.textBaseline = "middle"
						this.cx.shadowOffsetY = 1
						// var arr = [0,4,16,32]
						for(var i = 0; i < this.n; i++){
							this.map1[i] = []
							for(var j = 0; j < this.n; j++){
								this.map1[i][j] = {
									// num: arr[Math.floor(Math.random()*4)]
									num: 0
								}
							}
						}
						this.map0 = this.rotateArr(this.map1)
						this.map3 = this.rotateArr(this.map0)
						this.map2 = this.rotateArr(this.map3)
						this.map = this.map3
						this.slide(2)
						this.addEvents()
						this.start()
					},
					addEvents(){
						this.windowKeydown = evt => {
							var k = evt.key
							if(k === "a" || k === "A" || k === "ArrowLeft"){
								this.slide(3)
							}else if(k === "d" || k === "D" || k === "ArrowRight"){
								this.slide(1)
							}else if(k === "w" || k === "W" || k === "ArrowUp"){
								this.slide(0)
							}else if(k === "s" || k === "S" || k === "ArrowDown"){
								this.slide(2)
							}
						}
						w.addEventListener("keydown",this.windowKeydown)
					},
					draw(frame){
						this.timer && clearInterval(this.timer)
						this.timer = setInterval(() => {
							this.cx.save()
							this.cx.clearRect(0,0,this.mainWidth,this.mainWidth)
							this.bgShader()
							frame()
							this.cx.restore()
						},16)
					},
					bgShader(){
						this.cx.fillStyle = this.bgColor
						this.cx.fillRect(0,0,this.mainWidth,this.mainWidth)
					},
					cubicShader(x,y,num){
						this.cx.fillStyle = this.numColorMap[num]
						this.cx.fillRect(this.side * x + this.innerOffset, this.side * y + this.innerOffset, this.cWidth, this.cWidth)
						this.cx.fillStyle = "rgba(0,0,0,.5)"
						// this.cx.shadowBlur = 1
						// this.cx.shadowColor = "rgba(0,0,0,.4)"
						this.cx.fillText(num,this.side*(x+.5),this.side*(y+.5)+6)
						this.cx.fillStyle = "#fff"
						this.cx.fillText(num,this.side*(x+.5),this.side*(y+.5)+4)
						this.cx.shadowBlur = 0
						this.cx.shadowColor = "rgba(0,0,0,0)"
					},
					renderMap(){ // 上下左右 倒叙遍历
						for(let i = 0; i<this.n; i++){
							for(let j = 0; j <this.n; j++){
								this.map1[i][j].num && this.cubicShader(this.n-j-1,this.n-i-1,this.map1[i][j].num)
							}
						}
					},
					destroy(){
						this.timer && clearInterval(this.timer)
						this.cTimer && clearInterval(this.cTimer)
						this.canvas.isUsed = false
						w.removeEventListener("keydown",this.windowKeydown)
					},
					// ********************  操作逻辑部分
					slide(direction){
						for(let i = 0; i < this.n; i++){
							let stack = []
							for(let j = 0; j < this.n; j++){
								// 按列遍历
								var curr = this["map"+direction][i][j]
								// 判断 curr.num 是否与栈顶数相等
								if(stack.length === 0 && curr.num !== 0){
									stack.push(curr.num)
								}else if(curr.num === stack[stack.length-1]){
									stack[stack.length-1] += curr.num
								}else if(curr.num !== 0){
									stack.push(curr.num)
								}
							}
							for(let k = 0; k < this.n; k++){
								if(stack[k]){
									this["map"+direction][i][k].num = stack[k]
								}else{
									this["map"+direction][i][k].num = 0
								}
							}
						}
						this.isWin()
						this.supply(direction)
					},
					// slide 后补充方块
					supply(direction){
						var _this = this
						// var arr = []
						// for(let i = 0; i < this.n; i++){
						// 	arr[i] = []
						// 	for(let j = 0; j < this.n; j++){
						// 		arr[i][j] = this.map[i][j].num
						// 	}
						// }
						// console.table(arr) // 当前展示
						var hasSpace = false
						var addNum = 0
						for(let i = 0; i < this.n; i++){ // 遍历与滑动方向相反的那一侧的 4 个格子, 判断是否有剩余空间
							let curr = this["map"+cir(direction+1)][0][i]
							if(!curr.num){
								let isPut = Math.floor(Math.random()*2)
								if(isPut && addNum < 2){
									addNum++
									curr.num = 2
								}
								hasSpace = true
							}
						}
						if(!hasSpace){
							console.log("game over")
							logBoard.innerText = "Game over"
							clearInterval(this.cTimer)
						}
						function cir(index){
							return index % 4
						}
					},
					rotateArr(arr){
						var x = arr[0].length
						var y = arr.length
						var result = []
						for(var i=0;i<x;i++){
							result[i] = []
							for(var j=0;j<y;j++){
								result[i][j] = arr[y-j-1][i]
							}
						}
						return result
					},
					start(){
						this.draw(()=>{
							this.renderMap()
						})
					},
					roll(){
						var num = 0
						this.cTimer && clearInterval(this.cTimer)
						this.cTimer = setInterval(()=>{
							this.slide(cir(++num))
						},50)
						function cir(index){
							return index % 4
						}
					},
					stopRoll(){
						clearInterval(this.cTimer)
					},
					isWin(){
						for(let i = 0; i < this.n; i++){
							for(let j = 0; j < this.n; j++){
								if(this.map[i][j].num === 2048){
									clearInterval(this.cTimer)
									logBoard.innerText = "You win!!!"
									console.log("you win")
								}
							}
						}
					},
					// ********************  测试按钮
					
					testAll(){
						this.timer && clearInterval(this.timer)
						this.draw(()=>{
							this.cubicShader(0,0,2)
							this.cubicShader(0,1,4)
							this.cubicShader(0,2,8)
							this.cubicShader(0,3,16)
							this.cubicShader(1,0,32)
							this.cubicShader(1,1,64)
							this.cubicShader(1,2,128)
							this.cubicShader(1,3,256)
							this.cubicShader(2,0,512)
							this.cubicShader(2,1,1024)
							this.cubicShader(2,2,2048)
						})
					},
					
				})
				w.Game = Game
			}(window,document)
			var gameDemo = new Game()
		</script>
	</body>
</html>